package com.simpou.commons.utils.reflection;

import com.simpou.commons.utils.behavior.Conditions;
import com.simpou.commons.utils.reflection.condition.FieldCondition;
import com.simpou.commons.utils.file.FileHelper;
import com.simpou.commons.utils.lang.Conditionals;
import com.simpou.commons.utils.reflection.condition.DeclaredField;
import com.simpou.commons.utils.string.Strings;
import java.io.Serializable;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.NavigableSet;
import java.util.Queue;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import sun.reflect.generics.reflectiveObjects.TypeVariableImpl;

/**
 * Métodos úteis para manipulações de classes.
 *
 * @author Jonas Pereira
 * @since 2011-07-12
 * @version 2013-06-07
 */
public final class Reflections {

    /**
     * Prefixo usado para para preencher a assinatura de métodos. Um método que
     * tem 3 parâmetros teria na sua assinatura os parâmetros: p0,p1,p2.
     */
    public static final String PARAM_NAME_PREFIX = "p";

    /**
     * <p>getClassPath.</p>
     *
     * @param classDir Diretório de classes do projeto.
     * @param clasz Classe.
     * @return String Caminho absoluto do arquivo .class da classe.
     */
    public static String getClassPath(final String classDir,
            final Class<?> clasz) {
        String classDirAux = FileHelper.putDirPathEndDelim(classDir);
        String path = classDirAux
                + (clasz.getName().replaceAll("\\.", FileHelper.getDelimPattern()))
                + ".class";

        return path;
    }

    /**
     * <p>getSrcPath.</p>
     *
     * @param srcDir Diretório dos fontes do projeto.
     * @param clasz Classe.
     * @return String Caminho absoluto do arquivo .java da classe.
     */
    public static String getSrcPath(final String srcDir, final Class<?> clasz) {
        String classDirAux = FileHelper.putDirPathEndDelim(srcDir);
        String path = classDirAux
                + (clasz.getName().replaceAll("\\.", FileHelper.getDelimPattern()))
                + ".java";

        return path;
    }

    /**
     * <p>getReturnTypes.</p>
     *
     * @param method Método.
     * @return Tipos genéricos da classe de retorno do método. Null se retorno
     * não for genérico.
     */
    public static List<String> getReturnTypes(final Method method) {
        String returnTypeString = method.getGenericReturnType().toString();
        StringTokenizer tokenizer = new StringTokenizer(returnTypeString, "<,>");

        if (tokenizer.countTokens() < 2) {
            return null;
        }

        List<String> types = new ArrayList<String>();
        tokenizer.nextToken();

        while (tokenizer.hasMoreTokens()) {
            types.add(tokenizer.nextToken().trim());
        }

        return types;
    }

    /**
     * Não suporta modificadores de parâmetros e nem classes internas.
     *
     * @param method Método.
     * @param includeModifiers Incluir modificadores do método na assinatura.
     * @return Assinatura do método.
     */
    public static String getSignature(final Method method,
            final boolean includeModifiers) {
        //nome e retorno
        String name = method.getName();
        String returnType = method.getGenericReturnType().toString();
        String excludeString = "class ";

        if (returnType.contains(excludeString)) {
            returnType = returnType.replace(excludeString, "");
        }

        // parâmetros
        StringBuilder buffer = new StringBuilder();
        Class<?>[] paramsType = method.getParameterTypes();

        for (int i = 0; i < paramsType.length; i++) {
            buffer.append(paramsType[i].getCanonicalName())
                    .append(" " + PARAM_NAME_PREFIX).append(i + 1);

            if (i < (paramsType.length - 1)) {
                buffer.append(", ");
            }
        }

        // modificadores
        String signature = returnType + " " + name + "(" + buffer.toString()
                + ")";

        if (includeModifiers) {
            signature = Modifier.toString(method.getModifiers()) + " "
                    + signature;
        }

        //throws
        Class<?>[] exceptionTypes = method.getExceptionTypes();

        if (exceptionTypes.length > 0) {
            StringBuilder exceptions = new StringBuilder(" throws ");

            for (int i = 0; i < exceptionTypes.length; i++) {
                Class<?> exceptionType = exceptionTypes[i];
                exceptions.append(exceptionType.getCanonicalName());

                if (i < (exceptionTypes.length - 1)) {
                    exceptions.append(", ");
                }
            }

            signature += exceptions;
        }

        return signature.trim();
    }

    /**
     * <p>isClassGeneric.</p>
     *
     * @param clasz Classe.
     * @return Se a classe é genérica.
     */
    public static boolean isClassGeneric(final Class<?> clasz) {
        TypeVariable[] typeParameters = clasz.getTypeParameters();

        return typeParameters.length > 0;
    }

    /**
     * Valida se uma classe declara métodos.
     *
     * @param clasz Classe que deve declarar os métodos.
     * @param methods Métodos que devem ser declarados pela classe.
     * @return Se classe declara todos os métodos especificados.
     */
    public static boolean isDeclared(final Class<?> clasz,
            final Method... methods) {
        List<Method> decMethods = Arrays.asList(clasz.getDeclaredMethods());

        for (Method method : methods) {
            if (!decMethods.contains(method)) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param clasz Classe a ser verificada.
     * @return Todas interfaces que a classe implementa.
     */
    public static List<Class<?>> getInterfaces(final Class<?> clasz) {
        List<Class<?>> list = new ArrayList<Class<?>>();

        return getInterfaces(list, clasz);
    }

    /**
     * @param list Lista das interfaces já verificadas pela recursão.
     * @param clasz Classe a ser verificada.
     * @return Todas interfaces que a classe implementa.
     */
    private static List<Class<?>> getInterfaces(final List<Class<?>> list,
            final Class<?> clasz) {
        List<Class<?>> newList = Arrays.asList(clasz.getInterfaces());

        for (Class<?> classs : newList) {
            getInterfaces(list, classs);
        }

        list.addAll(newList);

        return list;
    }

    /**
     * Procura uma classe dentro de um array de classes. Útil, por exemplo, para
     * encontrar uma determinada interface implementada por uma classe ou
     * extendida por outra interface.
     *
     * @param classes Array de classes onde será procurada uma determinada
     * classe.
     * @param clasz Classe a ser buscada.
     * @return Classe encontrada ou null caso não esteja presente no array. Null
     * caso algum parâmetro seja null.
     */
    public static Class<?> searchClass(final Class<?>[] classes,
            final Class<?> clasz) {
        if ((classes == null) || (clasz == null)) {
            return null;
        }

        for (Class<?> classs : classes) {
            if (classs.equals(clasz)) {
                return classs;
            }
        }

        return null;
    }

    /**
     * Obtém o valor de um campo de um objeto via reflexão.
     *
     * @param obj Objeto.
     * @param fieldName Nome do campo do objeto.
     * @return Valor do campo do objeto.
     * @throws NoSuchFieldException Se houver.
     * @throws IllegalAccessException Se houver.
     */
    public static <T> T getValue(final Object obj, final String fieldName)
            throws NoSuchFieldException, IllegalAccessException {
        final Class<?> objClass = obj.getClass();
        final Field field = objClass.getDeclaredField(fieldName);
        T value = Casts.simpleCast(field.get(obj));

        return value;
    }

    /**
     * @param clasz Classe cujos campos são desejados.
     * @param conditions Condições de inclusão dos campos, campos que não
     * seguirem todas a condições serão descartados.
     * @return Campos da classe que seguem as condições. Campos das superclasses
     * são incluídos recursivamente.
     */
    public static List<Field> getFields(final Class<?> clasz, final Conditions<Field> conditions) {
        final Conditions<Field> conditionsAux = new Conditions<Field>();
        conditionsAux.add(new DeclaredField());
        conditionsAux.addAll(conditions);
        final List<Field> fields = new ArrayList<Field>();
        getFields(fields, clasz, conditionsAux);
        return fields;
    }

    /**
     * @param clasz Classe cujos campos são desejados.
     * @param conditions Condições de inclusão dos campos, campos que não
     * seguirem todas a condições serão descartados.
     * @return Campos da classe que seguem as condições. Campos das superclasses
     * são incluídos recursivamente.
     */
    public static List<Field> getFields(final Class<?> clasz, final FieldCondition... conditions) {
        Conditions<Field> listConditions = new Conditions<Field>();
        listConditions.addAll(Arrays.asList(conditions));
        return getFields(clasz, listConditions);
    }

    /**
     * Obtém o tipo de um campo. Útil em casos em que um campo é de um tipo
     * genérico que é perdido em tempo de execução ou que o tipo retornado pelo
     * campo é de seu herdeiro.
     *
     * @param field campo.
     * @param clasz Sub-classe da classe genérica que define o campo para o caso
     * de campo genéricos. Para os outros casos pode ser null ou a própria
     * classe que define o campo. Caso a classe genérica que declara o campo
     * tiver mais te um parâmetros generérico que foi definido pela sub-classe
     * não será possível determinar o tipo.
     * @return Se o campo não for genérico retorna seu tipo, senão retorna o
     * tipo parametrizado na superclasse, que ele está declarado, da classe
     * informada.
     */
    public static Class<?> getFieldType(final Class<?> clasz, final Field field) {
        final Class<?> curFieldType = field.getType();

        final Class<?> fieldType;

        final Type genericSuperclass = field.getGenericType();
        final Class<?> declaringClass = field.getDeclaringClass();
        if (genericSuperclass instanceof TypeVariable) {
            final ParameterizedType paramType = getSuperClassParameterizedType(declaringClass, clasz);
            if (paramType == null) {
                throw new UnsupportedOperationException("Cannot get field type because generic configuration is incompatible.");
            }
            Type actualTypeArgument = null;
            for (Type type : paramType.getActualTypeArguments()) {
                if (!(type instanceof TypeVariable)) {
                    if (actualTypeArgument != null) {
                        throw new UnsupportedOperationException("Multiple parametrization.");
                    } else {
                        actualTypeArgument = type;
                    }
                }
            }
            if (actualTypeArgument == null) {
                throw new UnsupportedOperationException("Cannot get field type because generic configuration is incompatible.");
            }
            fieldType = (Class<?>) actualTypeArgument;
        } else {
            fieldType = curFieldType;
        }

        return fieldType;
    }

    private static ParameterizedType getSuperClassParameterizedType(final Class<?> fieldClass, final Class<?> clasz) {
        final Type superclass = clasz.getGenericSuperclass();
        final ParameterizedType parameterizedType;
        if (superclass == null) {
            parameterizedType = null;
        } else if (superclass instanceof ParameterizedType) {
            final Type rawType = ((ParameterizedType) superclass).getRawType();
            if (rawType.equals(fieldClass)) {
                parameterizedType = (ParameterizedType) superclass;
            } else {
                final Class<?> typeClass = Casts.simpleCast(rawType);
                parameterizedType = getSuperClassParameterizedType(fieldClass, typeClass);
            }
        } else {
            final Class<?> typeClass = Casts.simpleCast(superclass);
            parameterizedType = getSuperClassParameterizedType(fieldClass, typeClass);
        }
        return parameterizedType;
    }

    /**
     * Verifica se um campo de uma classe segue as regras de POJOs. Em resumo
     * devem ser privados e ter métodos "get" e "set".
     *
     * @param field Campo a ser verificado.
     * @return true se campo segue todas regras de um POJO.
     */
    public static boolean isPojoField(final Field field) {
        if(Modifier.isStatic(field.getModifiers())){
            return false;
        }

        final Class<?> decClass = field.getDeclaringClass();

        final boolean isFinal = Modifier.isFinal(field.getModifiers());
        
        final String expFieldSignParam = "private "+(isFinal? "final " : "")+"{0} {1}.{2}";
        final String expFieldSign = Strings.replaceParams(expFieldSignParam, field.getType().getName(), decClass.getName(), field.getName());
        if (!expFieldSign.equals(field.toString())) {
            return false;
        }

        final String camelName = Strings.toUpperCaseFirstLetter(field.getName());
        Method getMethod, setMethod;
        final String setName = "set" + camelName;
        try {
            getMethod = Reflections.getGetterMethod(field);
            setMethod = isFinal ? null : decClass.getDeclaredMethod(setName, field.getType());
        } catch (NoSuchMethodException ex) {
            return false;
        } catch (SecurityException ex) {
            throw new RuntimeException(ex);
        }
        final String expGetMethodSign = Strings.replaceParams("public {0} {1}.{2}()", field.getType().getName(), decClass.getName(), getMethod.getName());
        final String expSetMethodSign = isFinal ? null : Strings.replaceParams("public void {1}.{2}({0})", field.getType().getName(), decClass.getName(), setName);

        return expGetMethodSign.equals(getMethod.toString())
                && (expSetMethodSign==null || expSetMethodSign.equals(setMethod.toString()));
    }

    public static Method getGetterMethod(final Field field) throws NoSuchMethodException {
        final String methodName;
        {
            final String preffix;
            if (field.getType().equals(boolean.class)) {
                preffix = "is";
            } else {
                preffix = "get";
            }
            methodName = preffix + Strings.toUpperCaseFirstLetter(field.getName());
        }
        return field.getDeclaringClass().getDeclaredMethod(methodName);
    }

    /**
     * @param objClass Classe do objeto a ser instanciado.
     * @return Nova instência do objeto.
     * @throws RuntimeException Se houver erro durante instanciação.
     */
    public static <T> T newInstance(final Class<T> objClass) {
        return newInstance(objClass,
                "Object from class " + objClass + " cannot be instantiated.");
    }

    /**
     * @param objClass Classe do objeto a ser instanciado.
     * @param errorMsg Mensagem de erro personalizada.
     * @return Nova instência do objeto.
     * @throws RuntimeException Se houver erro durante instanciação.
     */
    public static <T> T newInstance(final Class<T> objClass, final String errorMsg) {
        try {
            return objClass.newInstance();
        } catch (final Exception ex) {
            throw new RuntimeException(errorMsg, ex);
        }
    }

    /**
     * @param type Tipo da coleção. Tipos suportados: Collection, Queue, List,
     * Set, NavigableSet, SortedSet e qualquer implementação destas interfaces.
     * @return Instância de uma coleção.
     * @throws UnsupportedOperationException Se coleção não puder ser
     * instanciada.
     */
    public static Collection<Object> newCollectionInstance(final Class<?> type) {
        if (!type.isInterface()) {
            // implementações são instanciadas diretamente
            final Object objInstance = newInstance(type,
                    "Collection implementation " + type
                    + " cannot be instantiated.");
            final Collection<Object> instance = Casts.simpleCast(objInstance);

            return instance;
        }

        Collection<Object> instance;

        if (type.isAssignableFrom(Collection.class)
                || type.isAssignableFrom(List.class)
                || type.isAssignableFrom(Queue.class)) {
            instance = new LinkedList<Object>();
        } else if (type.isAssignableFrom(Set.class)) {
            instance = new HashSet<Object>();
        } else if (type.isAssignableFrom(NavigableSet.class)) {
            // NavigableSet e SortedSet
            instance = new TreeSet<Object>();
        } else {
            // dificilmente irá acontecer, método cobre todos os casos, talvez
            // em versão futura do Java
            throw new UnsupportedOperationException("Collection type " + type.getName()
                    + " not supported.");
        }

        return instance;
    }

    /**
     * Obtém um campo de uma classe. Este campo pode estar contido em uma de
     * suas superclasses.
     *
     * @param clasz Classe.
     * @param fieldName Nome do campo da classe ou de suas super classes.
     * @return Campo encontrado ou null.
     */
    public static Field getField(final Class<?> clasz, final String fieldName) {
        try {
            return clasz.getDeclaredField(fieldName);
        } catch (NoSuchFieldException ex) {
            final Class<?> superclass = clasz.getSuperclass();
            if (superclass == null) {
                return null;
            } else {
                return getField(superclass, fieldName);
            }
        } catch (SecurityException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Busca os campos nas superclasses recursivamente.
     *
     * @see Reflections#getFields(java.lang.Class,
     * com.simpou.commons.utils.reflection.FieldCondition[])
     */
    private static void getFields(final List<Field> fields, final Class<?> clasz, final Conditions<Field> conditions) {
        final Field[] declaredFields = clasz.getDeclaredFields();
        for (Field field : declaredFields) {
            if (Conditionals.check(field, conditions)) {
                fields.add(field);
            }
        }
        if (clasz.getSuperclass() != null) {
            getFields(fields, clasz.getSuperclass(), conditions);
        }
    }
}
